#region System
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Threading;
using CF.Framework.Engine.Controls;
using CF.Framework.Engine.Drawing;
using CF.Framework.Engine.Screens;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
#endregion

namespace CF.Framework.Engine.Screens
{
	/// <summary>
	/// The screen manager is a component which manages one or more <see cref="GameScreen"/>
	/// instances. It maintains a stack of _screens, calls their Update and Draw
	/// methods at the appropriate times, and automatically routes _input to the
	/// topmost active screen.
	/// </summary>
	public class ScreenManager : DrawableGameComponent
	{
		#region Fields

		private IGraphicsDeviceService graphicsDeviceService;
		private List<GameScreen> screens;
		private List<GameScreen> screensToUpdate;
		private List<GameScreen> screensToDraw;
		private List<GameScreen> screensToDispose;
		private InputState input = new InputState();
		private Texture2D blankTexture;
		private Timer timer;

		private bool isInitialized;

		#endregion

		#region Properties

		public Vector2 ScreenCenter
		{
			get
			{
				return new Vector2(GraphicsDevice.Viewport.Width / 2f,
								   GraphicsDevice.Viewport.Height / 2f);
			}
		}

		public int ScreenWidth
		{
			get { return GraphicsDevice.Viewport.Width; }
		}

		public int ScreenHeight
		{
			get { return GraphicsDevice.Viewport.Height; }
		}

		public SpriteFonts SpriteFonts { get; private set; }

		/// <summary>
		/// A default SpriteBatch shared by all the screens. This saves
		/// each screen having to bother creating their own local instance.
		/// </summary>
		internal SpriteBatch SpriteBatch { get; private set; }

		#endregion

		#region Initialization


		/// <summary>
		/// Constructs a new screen manager component.
		/// </summary>
		public ScreenManager(Game game)
			: base(game)
		{
			this.graphicsDeviceService = (IGraphicsDeviceService)game.Services.GetService(typeof(IGraphicsDeviceService));

			if (this.graphicsDeviceService == null)
			{
				throw new InvalidOperationException("No graphics device service.");
			}

			this.screens = new List<GameScreen>();
			this.screensToUpdate = new List<GameScreen>();
			this.screensToDraw = new List<GameScreen>();
			this.screensToDispose = new List<GameScreen>();

			game.Exiting += Game_Exiting;

			// we must set EnabledGestures before we can query for them, but
			// we don't assume the game wants to read them.

			// init timer
			timer = new Timer((state) =>
			{
				while (screensToDispose.Count != 0)
				{
					var screen = screensToDispose[0];

					screensToDispose.RemoveAt(0);

					screen.Dispose();
				}
			}, null, 2000, 23);

		}

		private void Game_Exiting(object sender, EventArgs e)
		{
			//Make sure to dispose ALL screens when the game is forcefully closed
			//We do this to ensure that open resources and threads created by screens are closed.
			foreach (GameScreen screen in screens)
			{
				screen.UnloadContent();
				screen.Dispose();
			}

			screens.Clear();
			screensToUpdate.Clear();
		}

		/// <summary>
		/// Initializes the screen manager component.
		/// </summary>
		public override void Initialize()
		{
			base.Initialize();

			isInitialized = true;
		}

		/// <summary>
		/// Load your graphics content.
		/// </summary>
		protected override void LoadContent()
		{
			SpriteBatch = new SpriteBatch(GraphicsDevice);
			SpriteFonts = new Screens.SpriteFonts();

			// Tell each of the _screens to load their content.
			foreach (GameScreen screen in screens)
			{
				screen.Initialize();
				screen.LoadContent();
				screen.Ready();
			}
		}

		/// <summary>
		/// Unload your graphics content.
		/// </summary>
		protected override void UnloadContent()
		{
			// Tell each of the screens to unload their content.
			foreach (GameScreen screen in screens)
			{
				screen.UnloadContent();
			}
		}

		#endregion

		#region Update and Draw
		/// <summary>
		/// Allows each screen to run logic.
		/// </summary>
		public override void Update(GameTime gameTime)
		{
			// Read the keyboard and gamepad.
			input.Update();

			// Make a copy of the master screen list, to avoid confusion if
			// the process of updating one screen adds or removes others.
			screensToUpdate.Clear();
			screensToUpdate.AddRange(screens.OrderByDescending(p => p.Index));

			bool otherScreenHasFocus = !Game.IsActive;
			bool coveredByOtherScreen = false;

			// Loop as long as there are screens waiting to be updated.
			while (screensToUpdate.Count > 0)
			{
				// Pop the topmost screen off the waiting list.
				GameScreen screen = screensToUpdate[screensToUpdate.Count - 1];

				screensToUpdate.RemoveAt(screensToUpdate.Count - 1);

				// Update the screen.
				screen.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

				if (screen.ScreenState == ScreenState.TransitionOn ||
					screen.ScreenState == ScreenState.Active)
				{
					// If this is the first active screen we came across,
					// give it a chance to handle input.
					if (!otherScreenHasFocus)
					{
						screen.DoHandleInput(input);

						otherScreenHasFocus = true;
					}

					// If this is an active non-popup, inform any subsequent
					// screens that they are covered by it.
					if (!screen.IsPopup)
						coveredByOtherScreen = true;
				}
			}
		}

		/// <summary>
		/// Tells each screen to draw itself.
		/// </summary>
		public override void Draw(GameTime gameTime)
		{
			screensToDraw.Clear();
			screensToDraw.AddRange(screens.OrderByDescending(p => p.Index));

			while (screensToDraw.Count > 0)
			{
				// Pop the topmost screen off the waiting list.
				GameScreen screen = screensToDraw[0];

				screensToDraw.RemoveAt(0);

				if (screen.ScreenState == ScreenState.Hidden)
				{
					continue;
				}

				// Update the screen.
				screen.Draw(gameTime);
			}
		}

		#endregion

		#region Public Methods

		public Vector2 MeasureCenter(Vector2 size)
		{
			return new Vector2((this.ScreenWidth - size.X) / 2, (this.ScreenHeight - size.Y) / 2);
		}

		public void Clear()
		{
			var list = screens.ToArray();
			foreach (var screen in list)
			{
				RemoveScreen(screen);
			}
		}

		public void AddScreen(GameScreen screen)
		{
			AddScreen(screen, null);
		}

		/// <summary>
		/// Adds a new screen to the screen manager.
		/// </summary>
		public void AddScreen(GameScreen screen, PlayerIndex? controllingPlayer = null)
		{
			screen.ControllingPlayer = controllingPlayer;

			// If we have a graphics device, tell the screen to load content.
			if (isInitialized)
			{
				screen.Initialize();
				screen.LoadContent();
				screen.Ready();
			}

			screens.Add(screen);
		}

		/// <summary>
		/// Removes a screen from the screen manager. You should normally
		/// use GameScreen.ExitScreen instead of calling this directly, so
		/// the screen can gradually transition off rather than just being
		/// instantly removed.
		/// </summary>
		public void RemoveScreen(GameScreen screen)
		{
			// If we have a graphics device, tell the screen to unload content.
			if (isInitialized)
			{
				screen.UnloadContent();
			}

			screens.Remove(screen);
			screensToUpdate.Remove(screen);
			screensToDraw.Remove(screen);

			// if there is a screen still in the manager, update TouchPanel
			// to respond to gestures that screen is interested in.

			screensToDispose.Add(screen);

			screen.IsUnloading = true;
		}

		/// <summary>
		/// Expose an array holding all the screens. We return a copy rather
		/// than the real master list, because screens should only ever be added
		/// or removed using the AddScreen and RemoveScreen methods.
		/// </summary>
		public GameScreen[] GetScreens()
		{
			return screens.ToArray();
		}

		/// <summary>
		/// Helper draws a translucent black fullscreen sprite, used for fading
		/// screens in and out, and for darkening the background behind popups.
		/// </summary>
		public void FadeBackBufferToBlack(float alpha)
		{
			Viewport viewport = GraphicsDevice.Viewport;

			SpriteBatch.Begin();

			SpriteBatch.Draw(blankTexture,
							 new Rectangle(0, 0, viewport.Width, viewport.Height),
							 Color.Black * alpha);

			SpriteBatch.End();
		}

		/// <summary>
		/// Informs the screen manager to serialize its state to disk.
		/// </summary>
		public void SerializeState()
		{
			// open up isolated storage
			using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
			{
				// if our screen manager directory already exists, delete the contents
				if (storage.DirectoryExists("ScreenManager"))
				{
					DeleteState(storage);
				}

				// otherwise just create the directory
				else
				{
					storage.CreateDirectory("ScreenManager");
				}

				// create a file we'll use to store the list of screens in the stack
				using (IsolatedStorageFileStream stream = storage.CreateFile("ScreenManager\\ScreenList.dat"))
				{
					using (BinaryWriter writer = new BinaryWriter(stream))
					{
						// write out the full name of all the types in our stack so we can
						// recreate them if needed.
						foreach (GameScreen screen in screens)
						{
							if (screen.IsSerializable)
							{
								writer.Write(screen.GetType().AssemblyQualifiedName);
							}
						}
					}
				}

				// now we create a new file stream for each screen so it can save its state
				// if it needs to. we name each file "ScreenX.dat" where X is the index of
				// the screen in the stack, to ensure the files are uniquely named
				int screenIndex = 0;
				foreach (GameScreen screen in screens)
				{
					if (screen.IsSerializable)
					{
						string fileName = string.Format("ScreenManager\\Screen{0}.dat", screenIndex);

						// open up the stream and let the screen serialize whatever state it wants
						using (IsolatedStorageFileStream stream = storage.CreateFile(fileName))
						{
							screen.Serialize(stream);
						}

						screenIndex++;
					}
				}
			}
		}

		public bool DeserializeState()
		{
			// open up isolated storage
			using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication())
			{
				// see if our saved state directory exists
				if (storage.DirectoryExists("ScreenManager"))
				{
					try
					{
						// see if we have a screen list
						if (storage.FileExists("ScreenManager\\ScreenList.dat"))
						{
							// load the list of screen types
							using (IsolatedStorageFileStream stream =
								storage.OpenFile("ScreenManager\\ScreenList.dat", FileMode.Open,
								FileAccess.Read))
							{
								using (BinaryReader reader = new BinaryReader(stream))
								{
									while (reader.BaseStream.Position < reader.BaseStream.Length)
									{
										// read a line from our file
										string line = reader.ReadString();

										// if it isn't blank, we can create a screen from it
										if (!string.IsNullOrEmpty(line))
										{
											Type screenType = Type.GetType(line);
											GameScreen screen = Activator.CreateInstance(screenType) as GameScreen;
											AddScreen(screen, PlayerIndex.One);
										}
									}
								}
							}
						}

						// next we give each screen a chance to deserialize from the disk
						for (int i = 0; i < screens.Count; i++)
						{
							string filename = string.Format("ScreenManager\\Screen{0}.dat", i);
							using (IsolatedStorageFileStream stream =
								storage.OpenFile(filename, FileMode.Open, FileAccess.Read))
							{
								screens[i].Deserialize(stream);
							}
						}

						return true;
					}
					catch (Exception)
					{
						// if an exception was thrown while reading, odds are we cannot recover
						// from the saved state, so we will delete it so the game can correctly
						// launch.
						DeleteState(storage);
					}
				}
			}

			return false;
		}

		/// <summary>
		/// Deletes the saved state files from isolated storage.
		/// </summary>
		private void DeleteState(IsolatedStorageFile storage)
		{
			// get all of the files in the directory and delete them
			string[] files = storage.GetFileNames("ScreenManager\\*");
			foreach (string file in files)
			{
				storage.DeleteFile(Path.Combine("ScreenManager", file));
			}
		}

		#endregion
	}
}